package com.peas.platform.module.icims.base.sys.component;

import cn.hutool.core.util.StrUtil;
import com.peas.platform.common.core.utils.MultipartFileUtil;
import com.peas.platform.module.icims.base.sys.api.resp.InvoiceResp;
import com.peas.platform.module.icims.base.sys.component.vo.PdfBoxKeyWordPosition;
import com.peas.platform.module.icims.base.sys.dto.AttachmentDto;
import com.peas.platform.module.icims.base.sys.service.AttachmentService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.PDFTextStripperByArea;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.awt.*;
import java.io.File;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 专用于处理电子发票识别的类
 */
@Component
@Slf4j
public class InvoiceExtractor {

    @Resource
    private AttachmentService attachmentService;

    // InputStream 不能重复读
    @SneakyThrows
    private InvoiceResp extract(String allText, InputStream is) {
        InvoiceResp v = new InvoiceResp();
        PDDocument document = PDDocument.load(is);
        PDPage firstPage = document.getDocumentCatalog().getPages().get(0);
        PDFTextStripperByArea stripper = new PDFTextStripperByArea();
        {
            // 去除空格，全角符号替换成半角符号
            allText = allText.replaceAll(" ", "")
                    .replaceAll("　", "")
                    .replaceAll("（", "(")
                    .replaceAll("）", ")")
                    .replaceAll("￥", "¥")
                    .replaceAll("：", ":")
                    .replaceAll(" ", "");
            v.setAllText(allText);

            // 通过全文中可以识别的内容先进行识别；
            // 提取发票标题
            String rr3 = "\\S*普通发票";
            Pattern p3 = Pattern.compile(rr3);
            Matcher m3 = p3.matcher(allText);
            if (m3.find()) {
                log.debug("标题:" + m3.group(0));
                v.setTitle(allText);
            }
            // 识别机器编号
            String reg = "机器编号:(?<jqbh>\\d{12})|发票代码:(?<fpdm>\\d{12})|发票号码:(?<fphm1>\\d{8})|发票号码:(?<fphm2>\\d{7})|开票日期:(?<kprq>\\d{4}年\\d{2}月\\d{2}日)|校验码:(?<jym>\\d{20})";
            Pattern pattern = Pattern.compile(reg);
            Matcher matcher = pattern.matcher(allText);
            while (matcher.find()) {
                if (matcher.group("jqbh") != null) {
                    log.debug("机器编号:" + matcher.group("jqbh"));
                    v.setInvoiceMachine(matcher.group("jqbh"));
                } else if (matcher.group("fpdm") != null) {
                    log.debug("发票代码:" + matcher.group("fpdm"));
                    v.setInvoiceCode(matcher.group("fpdm"));
                } else if (matcher.group("fphm1") != null) {
                    log.debug("发票号码:" + matcher.group("fphm1"));
                    v.setInvoiceNumber(matcher.group("fphm1"));
                } else if (matcher.group("fphm2") != null) {
                    log.debug("发票号码:" + matcher.group("fphm2"));
                    v.setInvoiceNumber(matcher.group("fphm2"));
                } else if (matcher.group("kprq") != null) {
                    log.debug("开票日期:" + matcher.group("kprq"));
                    v.setInvoiceDate(matcher.group("kprq"));
                } else if (matcher.group("jym") != null) {
                    log.debug("校验码:" + matcher.group("jym"));
                    v.setInvoiceValid(matcher.group("jym"));
                }
            }

            // 识别购买方
            // 识别销售方
            // 识别金额
            String rr1 = "价税合计\\u0028大写\\u0029(?<dxje>\\S*)\\u0028小写\\u0029¥(?<xxje>\\S*)\\s";
            Pattern p1 = Pattern.compile(rr1);
            Matcher m1 = p1.matcher(allText);
            if (m1.find()) {
                log.debug("价税合计(大写):" + m1.group("dxje"));
                v.setInvoiceTotalText(m1.group("dxje"));
                log.debug("价税合计:" + m1.group("xxje"));
                v.setInvoiceTotal(new BigDecimal(m1.group("xxje")));
            }
            // 识别底部
            String rr2 = "收款人:(?<skr>\\S*)复核:(?<fh>\\S*)开票人:(?<kpr>\\S*)销售方";
            Pattern p2 = Pattern.compile(rr2);
            Matcher m2 = p2.matcher(allText);
            if (m2.find()) {
                log.debug("收款人:" + m2.group("skr"));
                v.setInvoicePayee(m2.group("skr"));
                log.debug("复核:" + m2.group("fh"));
                v.setInvoiceApprover(m2.group("fh"));
                log.debug("开票人:" + m2.group("kpr"));
                v.setInvoicePrinter(m2.group("kpr"));
            }
            //判断发票类型：有“通行费”+“车牌号”两个关键字，判断为通行费。其他为一般发票
            if (allText.indexOf("通行费") > 0 && allText.indexOf("车牌号") > 0) {
                log.debug("车牌号通行费");
            } else {
                log.debug("普通");
            }
            //选定关键字作为坐标参考点
        }
        {
            // 获取几个重点关键词的坐标，依靠坐标去调整位置
            // 用这个的y坐标，限定购买方、密码区的y坐标
            PdfBoxKeyWordPosition kwp1 = new PdfBoxKeyWordPosition("机器编号");
            List<float[]> list1 = kwp1.getCoordinate(document);
            // 数组：1是x，0是y
            float[] jqbhCor = list1.get(0);
            // log.debug("关键字(机器编号)坐标：y:" + jqbhCor[0] + ", x:" + jqbhCor[1]);

            //原来用货物服务来做参考，发现有发票没有这个内容，改成用“税率”来参考
            // 用这个的y坐标，来限定明细的表格的y坐标
            PdfBoxKeyWordPosition kwp2 = new PdfBoxKeyWordPosition("税率");
            List<float[]> list2 = kwp2.getCoordinate(document);
            float[] hwhysCor = list2.get(0);
            // log.debug("关键字（货物或应税）坐标：y:" + hwhysCor[0] + ", x:" + hwhysCor[1]);

            //使用y坐标，作为销售方区域的y坐标参考
            PdfBoxKeyWordPosition kwp3 = new PdfBoxKeyWordPosition("价税合计");
            List<float[]> list3 = kwp3.getCoordinate(document);
            float[] jshjCor = list3.get(0);
            // log.debug("关键字（价税合计）坐标：y:" + jshjCor[0] + ", x:" + jshjCor[1]);

            // 用这个的x坐标，限定购买方和销售方的x坐标
            PdfBoxKeyWordPosition kwp4 = new PdfBoxKeyWordPosition("开户行及账号");
            List<float[]> list4 = kwp4.getCoordinate(document);
            float[] gmfkhhjzhCor;
            float[] xsfkhhjzhCor;
            if (list4.size() < 2) {
                // 获取不到，有变态的发票没法匹配，设置默认值
                gmfkhhjzhCor = new float[]{122, 51};
                xsfkhhjzhCor = new float[]{341, 51};
            } else {
                gmfkhhjzhCor = list4.get(0);
                xsfkhhjzhCor = list4.get(1);
            }

            // 选定密码区的x坐标参考点
            // 找出“密码区”的坐标。密码区三个字是独立的，竖着的。需要每个字单独找出来，还有可能有重复的。
            PdfBoxKeyWordPosition k1 = new PdfBoxKeyWordPosition("密");
            List<float[]> l1 = k1.getCoordinate(document);
            PdfBoxKeyWordPosition k2 = new PdfBoxKeyWordPosition("码");
            List<float[]> l2 = k2.getCoordinate(document);
            PdfBoxKeyWordPosition k3 = new PdfBoxKeyWordPosition("区");
            List<float[]> l3 = k3.getCoordinate(document);
            // 找出三个数组中x相同。定义为差值在5以内的。
            int maqX = 0;
            for (float[] floats : l1) {
                float x1 = floats[1];
                for (float[] value : l2) {
                    float x2 = value[1];
                    if (Math.abs(x1 - x2) < 5) {
                        // 认为相同
                        for (float[] item : l3) {
                            float x3 = item[1];
                            if (Math.abs(x2 - x3) < 5) {
                                maqX = Math.round((x1 + x2 + x3) / 3);
                            }
                        }
                    }
                }
            }
            // 给默认值
            if (maqX == 0) {
                maqX = 370;
            }
            // 找"规格型号"四个字，如果找不到，就找”规则型号“四个字。再找不到就默认值
            PdfBoxKeyWordPosition kwp5 = new PdfBoxKeyWordPosition("规格型号");
            List<float[]> list5 = kwp5.getCoordinate(document);
            int ggxhX = 175;
            if (list5.isEmpty()) {
                // 获取不到，就是发票有问题，现在碰到有写 规则型号的
                kwp5 = new PdfBoxKeyWordPosition("规则型号");
                list5 = kwp5.getCoordinate(document);
                if (!list5.isEmpty()) {
                    ggxhX = Math.round(list5.get(0)[1]);
                }
            } else {
                ggxhX = Math.round(list5.get(0)[1]);
            }
            log.debug("规格型号:" + ggxhX);
            // 坐标参考点获取完毕。


            // 下面开始按照坐标参考点切割区域读取内容
            int pageWidth2 = Math.round(firstPage.getCropBox().getWidth());
            int pageHeight2 = Math.round(firstPage.getCropBox().getHeight());
            // 开始按区域读取内容
            stripper.setSortByPosition(true);
            // 购买方：
            int x1 = Math.round(gmfkhhjzhCor[1]) - 15;    //开户行及账号的x为参考
            int y1 = Math.round(jqbhCor[0]) + 10;            //机器编号的y坐标为参考
            int w1 = maqX - x1 - 5;                            //密码区x坐标为参考
            int h1 = Math.round(gmfkhhjzhCor[0]) - y1 + 20;    //开户行及账号的y坐标为参考
            Rectangle gmf = new Rectangle(x1, y1, w1, h1);
            stripper.addRegion("GMF", gmf);

            // 销售方
            int x2 = Math.round(xsfkhhjzhCor[1]) - 15;        //开户行及账号为x参考
            int y2 = Math.round(jshjCor[0]) + 10;            //价税合计的y坐标为参考
            int w2 = maqX - x2 - 5;                            //密码区的x为参考
            int h2 = Math.round(xsfkhhjzhCor[0]) - y2 + 20;    //开户行及账号的y为参考
            Rectangle xsf = new Rectangle(x2, y2, w2, h2);
            stripper.addRegion("XSF", xsf);

            // 密码区
            int x3 = maqX + 10;                                //密码区的x为参考
            int y3 = Math.round(jqbhCor[0]) + 10;            //机器编号的y为参考
            int w3 = pageWidth2 - maqX - 10;                    //页面宽度减去密码区的x坐标为参考
            int h3 = Math.round(hwhysCor[0]) - y3;            //用税率的y坐标减去机器编号的y坐标作为参考
            Rectangle mmq = new Rectangle(x3, y3, w3, h3);
            stripper.addRegion("MMQ", mmq);

            // 购买明细,这里是除了名称之外的
            // 这里三个区域，要算出y，用“货物或应税”的y坐标做基础
            // 第二个基础，计算规格
            int x = ggxhX - 20;                                            //用加税合计的大写金额作为x坐标的参考
            int y = Math.round(hwhysCor[0]) + 10;                        //用税额的y坐标作参考
            int h = Math.round(jshjCor[0]) - Math.round(hwhysCor[0]) - 15;    //价税合计的y坐标减去税率的y坐标
            Rectangle gmxx = new Rectangle(x, y, pageWidth2, h);
            stripper.addRegion("GMXX", gmxx);

            // 商品名称
            Rectangle spmc = new Rectangle(0, y, x, h);        //
            stripper.addRegion("SPMC", spmc);


            // 重新打开一次文件，读取全部交易明细
            int pageWidth3 = Math.round(firstPage.getCropBox().getWidth());
            int pageHeight3 = Math.round(firstPage.getCropBox().getHeight());
            // 购买明细的全部，包含名称和价格等
            Rectangle qbmx = new Rectangle(0, y, pageWidth3, h);
            stripper.setSortByPosition(true);
            stripper.addRegion("QBMX", qbmx);


            stripper.extractRegions(firstPage);

            // 购买方
            String gmfString = stripper.getTextForRegion("GMF")
                    .replaceAll(" ", "")
                    .replaceAll("　", "")
                    .replaceAll("：", ":")
                    .replaceAll(" ", "");
            String reg1 = "名称:(?<mc>\\S*)|纳税人识别号:(?<nsrsbh>\\S*)|地址、电话:(?<dzdh>\\S*)|开户行及账号:(?<khhjzh>\\S*)";
            Pattern patternGmf = Pattern.compile(reg1);
            Matcher matcherGmf = patternGmf.matcher(gmfString);
            while (matcherGmf.find()) {
                // log.debug(matcherGmf.group(0));
                if (matcherGmf.group("mc") != null) {
                    log.debug("购买方名称:" + matcherGmf.group("mc"));
                    v.setInvoiceBuyerName(matcherGmf.group("mc"));
                } else if (matcherGmf.group("nsrsbh") != null) {
                    log.debug("购买方纳税人识别号:" + matcherGmf.group("nsrsbh"));
                    v.setInvoiceBuyer(matcherGmf.group("nsrsbh"));
                } else if (matcherGmf.group("dzdh") != null) {
                    log.debug("购买方地址电话:" + matcherGmf.group("dzdh"));
                    v.setInvoiceBuyerInfo(matcherGmf.group("dzdh"));
                } else if (matcherGmf.group("khhjzh") != null) {
                    log.debug("购买方开户行及账号:" + matcherGmf.group("khhjzh"));
                    v.setInvoiceBuyerBank(matcherGmf.group("khhjzh"));
                }
            }


            // 销售方
            String xsfString = stripper.getTextForRegion("XSF")
                    .replaceAll(" ", "")
                    .replaceAll("　", "")
                    .replaceAll("：", ":")
                    .replaceAll(" ", "");
            String reg2 = "名称:(?<mc>\\S*)|纳税人识别号:(?<nsrsbh>\\S*)|地址、电话:(?<dzdh>\\S*)|开户行及账号:(?<khhjzh>\\S*)";
            Pattern patternXsf = Pattern.compile(reg2);
            Matcher matcherXsf = patternXsf.matcher(xsfString);
            while (matcherXsf.find()) {
                // log.debug(matcherXsf.group(0));
                if (matcherXsf.group("mc") != null) {
                    log.debug("销售方名称:" + matcherXsf.group("mc"));
                    v.setInvoiceSellerName(matcherXsf.group("mc"));
                } else if (matcherXsf.group("nsrsbh") != null) {
                    log.debug("销售方纳税人识别号:" + matcherXsf.group("nsrsbh"));
                    v.setInvoiceSeller(matcherXsf.group("nsrsbh"));
                } else if (matcherXsf.group("dzdh") != null) {
                    log.debug("销售方地址电话:" + matcherXsf.group("dzdh"));
                    v.setInvoiceSellerInfo(matcherXsf.group("dzdh"));
                } else if (matcherXsf.group("khhjzh") != null) {
                    log.debug("销售方开户行及账号:" + matcherXsf.group("khhjzh"));
                    v.setInvoiceSellerBank(matcherXsf.group("khhjzh"));
                }
            }


            // 密码区
            String mmqString = stripper.getTextForRegion("MMQ")
                    .replaceAll(" ", "")
                    .replaceAll("　", "");
            // 过滤不需要的，包括第一行可能是校验码，最后一行空行
            String[] passArr = mmqString.split("\\s");
            StringBuilder newMmqString = new StringBuilder();
            for (int i = 0; i < passArr.length; i++) {
                String pass = passArr[i];
                if (pass.startsWith("校") || pass.isEmpty())
                    continue;
                // 先把中文去掉
                String rr = "[\u4e00-\u9fa5]";
                Pattern pat = Pattern.compile(rr);
                Matcher mat = pat.matcher(pass);
                pass = mat.replaceAll("");
                if (pass.startsWith("校") || pass.isEmpty())
                    continue;
                newMmqString.append(pass);
                if (i < passArr.length - 1)
                    newMmqString.append("\n");
            }
            String pass = newMmqString.toString().replaceAll("\r\n", "").replaceAll("\n", "");
            log.debug("密码区:" + pass);
            v.setInvoicePassword(pass);

            // 处理商品名称。


            // log.debug("========商品名称========");
            // log.debug(spmcString);
            // 根据上面的itemsList判断名称应该有几行
        }
        {
            String spmcString = stripper.getTextForRegion("SPMC").replaceAll(" ", "").replaceAll("　", "");
            String[] nameArr = spmcString.split("[\\n\\r]");
            ArrayList<String> nameArrList = new ArrayList<>();
            // 先过滤名称的多余内容，变成单纯的名称的内容
            for (String string : nameArr) {
                if (string.contains("货物或") || string.contains("项目名称") || string.contains("开户行") || string.replaceAll(" ", "").contains("合计") || string.trim().isEmpty())
                    continue;
                nameArrList.add(string);
            }
            // 拿到全部的明细，包含名称和价格，用以处理真实的商品名称：后代带有税率价格的，是商品名称的第一行，没有的，则是第二行
            String qbmxString = stripper.getTextForRegion("QBMX").replaceAll(" ", "").replaceAll("　", "");
            // 理论上nameArr和 mxArr是一样多的行数
            String[] mxArr = qbmxString.split("[\\n\\r]");
            // 滤清明细表格内容
            ArrayList<String> mxArrList = new ArrayList<>();
            for (String s : mxArr) {
                if (s.indexOf("单价金额") > 0 || s.contains("开户行") || s.contains("合计") || s.trim().isEmpty())
                    continue;
                mxArrList.add(s);
            }
            StringBuilder spName = new StringBuilder();
            ArrayList<String> nameList = new ArrayList<>();
            for (int i = 0; i < mxArrList.size(); i++) {
                // log.debug("------" + mxArr[i]);
                if (mxArrList.get(i).matches("\\S+\\d*(%|免税|不征税|出口零税率|普通零税率)\\S*")) {
                    // log.debug("找到第一行");
                    if (StrUtil.isNotBlank(spName)) {
                        nameList.add(spName.toString());
                    }
                    // 判断为名称的第一行
                    spName = new StringBuilder(nameArrList.get(i));
                } else {
                    spName.append(nameArrList.get(i));
                }
            }
            nameList.add(spName.toString());
            // log.debug(nameList);
            // 把名称放到完整数组里面
            // 交易明细这里只处理除商品名称之外的列，相当于少一列
            String gmxxString = stripper.getTextForRegion("GMXX").replaceAll("　", " ");
            log.debug("------------购买明细-------------------");
            log.debug("gmxxString:" + gmxxString);
            ArrayList<Map<String, Object>> itemList = new ArrayList<>();
            // 拆开行
            String[] gmxxArr = gmxxString.split("[\\n\\r]");
            // 金额合计
            BigDecimal amount = new BigDecimal(0);
            // 税额合计
            BigDecimal tax = new BigDecimal(0);
            for (String value : gmxxArr) {
                // 清除不是合法的行
                if (value.replaceAll(" ", "").matches("\\S*(金额|税率|税额|¥|￥)\\S*") || value.length() < 2)
                    continue;
                log.debug("value" + value);
                // 目前发现，只有最后3个，分别是金额、税额是必须有的。并且税率出现“免税”字样，当免税的时候，税额出现过“***"，暂不知是否有其他字样
                // 查看规范，有：免税，不征税，出口零税率，普通零税率 四种
                // 税率可能是空，附带明细表述各种商品的税率，这里做一个特殊处理，这种情况，只能有两个：金额和税额，其他都不能有
                String[] itemArr = value.split("\\s");
                HashMap<String, Object> items = new HashMap<>();
                //特殊情况，只有两列，就必须是金额和税额
                if (itemArr.length == 2) {
                    items.put("je", new BigDecimal(itemArr[0]));
                    items.put("se", new BigDecimal(itemArr[1]));
                    amount = amount.add((BigDecimal) items.get("je"));
                    tax = tax.add((BigDecimal) items.get("se"));
                    itemList.add(items);
                    continue;
                }

                //处理 金额、税率、税额
                // 先处理倒数第三个 金额
                items.put("je", new BigDecimal(itemArr[itemArr.length - 3]));
                // 处理倒数第二个 税率，倒数第一个 税额
                // 如果不是%，就是免税
                if (itemArr[itemArr.length - 2].indexOf("免税") > 0 || itemArr[itemArr.length - 2].indexOf("不征税") > 0
                        || itemArr[itemArr.length - 2].indexOf("出口零税率") > 0
                        || itemArr[itemArr.length - 2].indexOf("普通零税率") > 0
                        || !itemArr[itemArr.length - 2].contains("%")) {
                    // 免税，设置为0
                    items.put("sl", new BigDecimal(0));
                    items.put("se", new BigDecimal(0));
                } else {
                    // 有税率
                    BigDecimal sl = new BigDecimal(Integer.parseInt(itemArr[itemArr.length - 2].replaceAll("%", "")));
                    // 除100
                    sl = sl.divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
                    items.put("sl", sl);
                    items.put("se", new BigDecimal(itemArr[itemArr.length - 1]));
                }

                // 表示找到的第几个数字
                int numIndex = 1;
                for (int j = 0; j < itemArr.length - 3; j++) {
                    // log.debug(itemArr[j]);
                    if (itemArr[j].matches("^(-?\\d+)(\\.\\d+)?$")) {
                        // 如果找到了数字，看是第几个，如果是第一个，就是 数量，第二个就是单价
                        if (numIndex == 1) {
                            items.put("liang", itemArr[j]);
                            numIndex++;
                        } else {
                            items.put("dj", itemArr[j]);
                        }
                    } else {
                        // 如果找到第一个文字，则看下面一个是否也是文字，如果也是，就是 规格和单位，如果只有一个，默认放到单位（目前样本看来，单位的更多）
                        if (itemArr.length >= j + 1 && !itemArr[j + 1].matches("^(-?\\d+)(\\.\\d+)?$")) {
                            items.put("dw", itemArr[j + 1]);
                            items.put("ggxh", itemArr[j]);
                            j++;
                        } else {
                            items.put("dw", itemArr[j]);
                        }
                    }
                }

                amount = amount.add((BigDecimal) items.get("je"));
                tax = tax.add((BigDecimal) items.get("se"));
                itemList.add(items);
            }
            for (int i = 0; i < nameList.size(); i++) {
                if (i >= itemList.size())
                    continue;
                itemList.get(i).put("hwmc", nameList.get(i));
            }
            log.debug("合计金额:" + amount);
            log.debug("税额:" + tax);
        }
        return v;
    }

    @SneakyThrows
    public InvoiceResp extract(MultipartFile file) {
        String allText = loadText(file.getInputStream());
        return extract(allText, file.getInputStream());
    }

    @SneakyThrows
    public InvoiceResp extract(AttachmentDto attachment) {
        String allText = loadText(attachmentService.getObject(attachment));
        return extract(allText, attachmentService.getObject(attachment));
    }

    @SneakyThrows
    public InvoiceResp extract(String pdfPath) {
        return extract(MultipartFileUtil.fileToCommonsMultipartFile(new File(pdfPath)));
    }

    @SneakyThrows
    private String loadText(MultipartFile multipartFile) {
        try (PDDocument doc = PDDocument.load(multipartFile.getInputStream())) {
            PDFTextStripper textStripper = new PDFTextStripper();
            textStripper.setSortByPosition(true);
            return textStripper.getText(doc);
        }
    }

    @SneakyThrows
    private String loadText(InputStream inputStream) {
        try (PDDocument doc = PDDocument.load(inputStream)) {
            PDFTextStripper textStripper = new PDFTextStripper();
            textStripper.setSortByPosition(true);
            return textStripper.getText(doc);
        }
    }

}
